authguidance.com

API Authorization Design – OAuth Architecture Guidance

Background

In our previous post we discussed User Data Management and in this post we will focus on data protection in APIs. Any real world system needs to apply business rules before allowing access to resources.

OAuth Capabilities

The OAuth family of specifications has many finer details, but I think of these as the two main capabilities:

Capability Description
Authentication Dealing with users and authentication, while externalizing this complexity from applications
Data Protection Design patterns that enable APIs to authorize access to resources based on tokens

API clients may use different authentication flows, but APIs always protect data in the same way, by receiving access tokens:

This post will focus on OAuth data protection and how it maps to complex business rules, which is an area that is often not properly understood.

APIs and Business Rules

A request to get data from a UI might look like this, where a token containing the subject claim is sent and then business rules are applied:

The above example might include handling business rules like these, and we will show how to manage these types of rule in an OAuth secured API:

API Authorization Steps

Implementing authorization in done via three phases but the main work is always done in step 3:

Step Description
Token Validation JWT Access Token Validation, to ensure integrity of the message credential
Scope Checks Sanity checks to ensure that an access token is allowed to be used for a particular business area
Claims Based Authorization Detailed permission checks against resources, using domain specific data

Step 1: Token Validation

The JWT Access Token Validation blog post described how an API verifies the JWT’s digital signature and if the token is expired. If the token is not valid a 401 error is returned:

"code": "unauthorized",
"message": "Missing, invalid or expired access token"

Think of token validation as an entry level check to authenticate the request to the API, after which the API can trust data in the access token’s payload.

Step 2: Scopes

Scopes can be included in access tokens to represent an Area of Data and Permissions on that Data.

Examples Usage
orders Indicates that an access token grants access to orders placed by a user
orders_read Indicates that an access token cannot be used to make data changes to orders

When personal assets are involved, it is usual to show a consent screen that displays scopes, so that the user knows which data they are granting access to, and whether they are granting read or write access. 

Built-In Scopes

OAuth also uses some built in scopes for Personally Identifiable Information (PII) that is stored by the Authorization Server:

Examples Usage Scenario
openid Indicates that the user’s identity is being used, via the OpenID Connect protocol
profile Indicates that the user’s name and possibly other information is being used

Scope Limitations

Scopes are fixed at design time and you cannot use them for dynamic purposes, such as different scopes for different types of user. Whenever you need to perform dynamic authorization, claims must be used.

Audience Checks

APIs should also check the audience of received access tokens, and the most common setup is for a set of related APIs to use the same audience. This enables JWT access tokens to be forwarded between microservices:

API Audience
Orders api.mycompany.com
Customers api.mycompany.com
Products api.mycompany.com

If you deal with different subdivisions of a large company, then it is usually recommended to use a different audience per subdivision.

Step 3: Claims

Our next code sample will authorize using the following custom claims. Note that none of this data is stored in the Authorization Server:

Claim Represents
User ID The user id with which transactions are stored in the domain specific data
User Role Two user roles are involved, for a normal user and an administrative user
User Regions An administrator grants users access to data for one or more regions, represented as an array

The third of these is an array claim, to represent the type of authorization that must be done in many real world business systems:

Claims Requirements

There are two main requirements that you need to consider when working with claims, and we will show two main ways to achieve this:

Requirement Description
API Data The API must receive the data it needs in order to implement its domain specific authorization
Confidentiality Access tokens returned to internet clients must not reveal all of this information

Claims Architecture Option 1

The ideal way to meet the above requirements is for the Authorization Server to reach out to the API at the time of token issuance to get custom claims, then to include the custom claims in the access token.

This state is then stored in the Authorization Server, and the access token returned to clients uses a confidential reference token format, which is typically a UUID or something similar:

When a client calls the API, the reference token is introspected to get a JWT access token, which is then forwarded to the API. The introspection is usually done in an API gateway that is placed in front of the API:

This is a great solution when supported, since all claims issued are audited by the Authorization Server and it scales very well if the JWT needs to be forwarded between microservices.

Claims Architecture Option 2

Option 1 may require a specialist Authorization Server, whereas this blog is using AWS Cognito by default, which does not support the above features.

We will therefore also demonstrate an alternative approach, where custom claims are looked up when an access token is first received, then cached:

All subsequent requests with the same access token can then be handled in a fast manner by quickly looking up extra claims from the memory cache:

A JWT access token issued by AWS Cognito for our second code example contains the following data. The access token is pretty confidential, so we are meeting that requirement:

Claims Principal

All of this blog’s APIs collect claims into a ClaimsPrincipal class. This should be designed early, to provide the information the API needs for authorization:

Authorizer Class

When using the Option 2 architecture we will use an Authorizer class to perform the claims lookup, populate the Claims Principal, and to deal with caching results:

Claims Caching Behaviour

Our next code sample will include some logging to demonstrate the above pattern. This highlights how **Claims Lookup **is only done when a new access token is received, and is very fast on subsequent requests:

The claims cache has the following characteristics:

This means that if a user’s permissions change, they can log off and log on again to get a new access token, after which their updated privileges will come into effect.

Business Authorization

The claims principal, or its domain specific claims, can then be injected into business logic classes:

The API can then deny access if unauthorized resources are requested, or filter collections to only include authorized items:

User Info Claims

I prefer to also avoid including sensitive data in ID tokens, which sit around in a UI for an entire user session. If possible I instead serve User Info via APIs, which is both more secure and more extensible:

Step Description
Confidential ID Tokens Avoid including personally identifiable information in ID tokens
Avoid Exposing OAuth User Info Avoid exposing the OAuth User Info endpoint to the internet, to prevent anyone with an access token being able to get PII
UIs call APIs to get User Info APIs can return both OAuth User Info and domain specific user data to UI clients

Where Are We?

We have described patterns for achieving Extensible Authorization in OAuth secured APIs. This blog’s next code sample will implement the claims caching pattern, and our final APIs will support both patterns.

Next Steps